/* 
 * File:   smtp_delivery.cpp
 * Author: james
 *
 * Created on 2011年3月23日, 下午2:43
 */

#include "smtp_delivery.h"
#include "maildrop.h"
#include "log.h"
#include "dns_query.h"

ssize_t write_n(int sockfd, const char* vptr, size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    
    ptr = vptr;
    nleft = n;
    while(nleft > 0)
    {
        if((nwritten = write(sockfd, ptr, nleft)) <= 0)
        {
            if(nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    
    return n;
}
ssize_t read_n(int fd, void* vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
    
    ptr = (char*)vptr;
    nleft = n;
    while(nleft > 0)
    {
        if((nread = read(fd, ptr, nleft)) < 0)
        {
            if(errno == EINTR)
                nread = 0;
            else
                return -1;
        }
        else if(nread == 0)
            break; //EOF
            
        nleft -= nread;
        ptr += nread;
    }
    
    return (n - nleft);    

}

smtp_delivery_t::smtp_delivery_t(const char* bind_ip):
m_bind_ip(bind_ip),m_smtp_code(250), m_mx_status(MX_S_OK),m_smtp_status(SMTP_OK),m_reason("OK")
{
    logtrace((MAILDROP, "smtp_delivery_t::smtp_delivery_t bind ip:<%s>", m_bind_ip.c_str()));
}

smtp_delivery_t::~smtp_delivery_t()
{
    logtrace((MAILDROP, "smtp_delivery_t::~smtp_delivery_t destruction ok."));
}

int smtp_delivery_t::connect_mx_i(vector<string>& mx_list)
{
    logtrace((MAILDROP, "smtp_delivery_t::connect_mx_i begin ..."));
   
    // validate local ip
    if(m_bind_ip.empty())
    {
        logerror((MAILDROP, "smtp_delivery_t::connect_mx_i bind ip is empty."));
        return -1;
    }
    
    in_addr_t my_addr = inet_addr(m_bind_ip.c_str());
    if (my_addr == INADDR_NONE) {
        // local_ip is not a valid IP address
        logerror((MAILDROP, "smtp_delivery_t::connect_mx_i bind ip is invalid."));
        return -1;
    }

    // create the sotkcet
    int sock = socket(PF_INET, SOCK_STREAM, getprotobyname("tcp")->p_proto);
    if (sock < 0) {
        logerror((MAILDROP, "smtp_delivery_t::connect_mx_i => socket failed:%s.", 
                 strerror(errno)));
        return -1;
    }

    // bind to specific IP address
    struct sockaddr_in local_addr;
    memset(&local_addr, 0, sizeof(struct sockaddr_in));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = 0;
    local_addr.sin_addr.s_addr = my_addr;
    if (bind(sock, (struct sockaddr *)&local_addr, sizeof(sockaddr_in))) {
        logerror((MAILDROP, "smtp_delivery_t::connect_mx_i => bind failed:%s.", 
                 strerror(errno)));
        return -1;
    }

    // connect to any remove MX
    bool connected = false;
    for (vector<string>::const_iterator it = mx_list.begin(); (it != mx_list.end() && !connected); it++) {
        logdebug((MAILDROP, "smtp_delivery_t::connect_mx_i trying hostname:<%s>", it->c_str()));
        // get IP from hostname
        struct hostent * mx = gethostbyname(it->c_str());
        if (mx == NULL) {
            // we don't care about single failure
            logdebug((MAILDROP, "smtp_delivery_t::connect_mx_i gethostbyname(%s):<%s>", 
                      it->c_str(), strerror(errno)));
            continue;
        }

        // try out every IP address for the specific MX
        for (int index=0; mx->h_addr_list[index] != NULL; index++) {
            logdebug((MAILDROP, "smtp_delivery_t::connect_mx_i try ip <%s> ...", 
                      inet_ntoa(*(struct in_addr*)mx->h_addr_list[index])));
            struct sockaddr_in remote_addr;
            remote_addr.sin_family = AF_INET;
            remote_addr.sin_port = htons(25);
            remote_addr.sin_addr = *((struct in_addr *)mx->h_addr_list[index]);

            if (!connect(sock, (struct sockaddr *)&remote_addr, sizeof(sockaddr_in))) {
                connected = true;
                break;
            } else {
                logdebug((MAILDROP, "smtp_delivery_t::connect_mx_i connect ip <%s> failed:<%s>", 
                      inet_ntoa(*(struct in_addr*)mx->h_addr_list[index]), strerror(errno)));
            }
        }
    }

    if (!connected) {
        logerror((MAILDROP, "smtp_delivery_t::connect_mx_i cannot connect any mx"));
        return -1;
    }

    return sock;    
}

int smtp_delivery_t::read_socket_i(int sock, std::string & msg, int timeout)
{
    logtrace((MAILDROP, "smtp_delivery_t::read_socket_i begin ..."));
   // make it a clean start
    msg.clear();

    // fd set for select (use select for timeout control)
    fd_set fds;

    FD_ZERO(&fds);
    FD_SET(sock, &fds);

    // timeout control
    struct timeval tv;
    tv.tv_sec = timeout;
    tv.tv_usec = 0;

    // start reading
    while (true) {
        int retval = select(sock+1, &fds, NULL, NULL, &tv);
        if (retval == -1) {
            if(errno == EINTR) continue;
            logerror((MAILDROP, "smtp_delivery_t::read_socket_i select failed:<%s>",
            strerror(errno)));
            return -1;
        } else if (retval == 0) {
            logerror((MAILDROP, "smtp_delivery_t::read_socket_i select timeout"));
            return -1;
        }

        char buffer[1024];
        int bytes_read = read(sock, buffer, 1024);
        if (bytes_read <= 0) {
            logerror((MAILDROP, "smtp_delivery_t::read_socket_i read failed:<%s>", 
                    strerror(errno)));
            return -1;
        }
        msg += std::string(buffer, bytes_read);
        if (msg.find_first_of("\n") != std::string::npos) {
            // got line end
            break;
        }
    }

    // remove CR and LF for better ... debugging?
    msg.erase(remove(msg.begin(), msg.end(), '\r'), msg.end());
    msg.erase(remove(msg.begin(), msg.end(), '\n')), msg.end();
    logtrace((MAILDROP, "smtp_delivery_t::read_socket_i end ok."));
    return 0;
}
void smtp_delivery_t::set_status_i(smtp_status_t smtp_status, mx_status_t mx_status, const char* fmt, ...)
{
    logtrace((MAILDROP, "smtp_delivery_t::set_status_i"));
    char msg[1024] = {0};
    
    va_list va;
    va_start(va, fmt);
    vsnprintf(msg, 1023, fmt, va);
    va_end(va);
    msg[1023] = '\0';
    
    char m2[1024] = {0};
    if(m_smtp_code == 250)
    {
        if(smtp_status == SMTP_FAILED) 
            m_smtp_code = 550;
        
        if(smtp_status == SMTP_RETRY) 
            m_smtp_code = 450;
    }
    snprintf(m2, 1023, "%d %s\n", m_smtp_code, msg);
    m_mx_status = mx_status;
    m_smtp_status = smtp_status;
    m_reason.assign(m2);
}

int smtp_delivery_t::get_smtp_code_i(const string& response, string& reason)
{
    logtrace((MAILDROP, "smtp_delivery_t::get_smtp_code_i"));
    std::string::size_type pos = response.find_first_not_of("0123456789");
    if ((pos == std::string::npos) || (pos <3)) {
        // first 3 characters must be digits
        return -1;
    }
    m_smtp_code = atoi(response.substr(0, 3).c_str());
    reason = response.substr(4);
    return m_smtp_code;
}

int smtp_delivery_t::send(
         const char* helo_host,
         const char* from,
         const char* to,
         const char* to_domain,
         long timeout,
         int msg_fd,
         vector<string>& mx)
{
    logtrace((MAILDROP, "smtp_delivery_t::send begin ..."));
    
    string reason;
    //! get dns mx list
    vector<string> mx_q;
    if(dns_query_t::mx_query(to_domain, mx_q, mx))
    {
        set_status_i(SMTP_FAILED, MX_S_TO_MX, 
                     "recipient address domain <%s> mx query failed",to_domain);
        logerror((MAILDROP, "smtp_delivery_t::send => mx_query failed."));
        return -1;
    }
    
    int mx_sock = connect_mx_i(mx_q);
    if(mx_sock < 0)
    {
        set_status_i(SMTP_FAILED, MX_S_TO_MX, 
                     " connect to mx domain <%s>  failed",to_domain);        
        logerror((MAILDROP, "smtp_delivery_t::send => connect_mx_i failed."));
        return -1;
    }
    
    string msg;
    //! read welcome response
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "recipient address domain <%s> mx query failed",to_domain);
        logerror((MAILDROP, "smtp_delivery_t::send => mx_query failed."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    // do HELO
    //msg = std::string("HELO ") + from_domain + "\r\n";
    msg = std::string("HELO ") + helo_host + "\r\n";
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing helo <%s> request",helo_host);
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_helo>."));
        return -1;
    }
    
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "timeout in reading <%s> welcome response",to_domain);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_helo>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    // make sure HELO returns what we expected
    int code = get_smtp_code_i(msg, reason);
    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_HELO, reason.c_str());               
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_helo>."));
        return -1;
    }

    // do MAIL FROM
    msg = std::string("MAIL FROM: <") + from + ">\r\n";
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing mail from <%s> request",from);
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_mail_from>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "mail from <%s> response timeout",from);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_mail_from>."));
        return -1;        
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code >= 500) {
        set_status_i(SMTP_FAILED, MX_S_SMTP_MAIL, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send blocked at <do_mail_from>."));
        return -1;
    }

    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_MAIL, reason.c_str()); 
        logerror((MAILDROP, "smtp_delivery_t::send temp failed at at <do_mail_from>."));
        return -1;
    }
    
    // do RCPT TO
    msg = std::string("RCPT TO: <") + to + ">\r\n";
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing rcpt to <%s> request",to);
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_rcpt_to>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "rcpt to <%s> response timeout",to);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_rcpt_to>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code >= 500) {
        set_status_i(SMTP_FAILED, MX_S_TO_USER, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send blocked at <do_rcpt_to>."));
        return -1;
    }
    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_RCPT, reason.c_str()); 
        logerror((MAILDROP, "smtp_delivery_t::send temp failed at <do_rcpt_to>."));
        return -1;
    }

    // do DATA
    msg = std::string("DATA\r\n");
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing data request");
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_data>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "data to <%s> response timeout",to);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_data>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code != 354) {
        set_status_i(SMTP_FAILED, MX_S_SMTP_DATA, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send <%s> blocked at <do_data>.",
                to));
        return -1;
    }

    // now send data over
    char buffer[4096] = {0};
    ssize_t readed = 0;
    while( (readed = read_n(msg_fd, buffer, 4096)) > 0)
    {
        if(write_n(mx_sock, buffer, readed) < 0)
        {
            set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                         "failed in writing data content request");
            logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_data_content>."));
            return -1;
        }
    }
        
    // finish up
    msg = std::string("\r\n.\r\n");
     if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing data end request");
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_data_dot>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "data end <%s> response timeout",to);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_data>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code >= 500) {
        set_status_i(SMTP_FAILED, MX_S_SMTP_DOT, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send blocked at <do_data_dot>."));
        return -1;
    }
    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_DOT, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send temp failed at <do_data_dot>."));
        return -1;
    }    
    logtrace((MAILDROP, "smtp_delivery_t::send end ok."));
    return 0;
}

int smtp_delivery_t::send(
         const char* helo_host,
         const char* from,
         const char* to,
         const char* to_domain,
         long timeout,
         string& data,
         vector<string>& mx)
{
    logtrace((MAILDROP, "smtp_delivery_t::send begin ..."));
    
    //! get dns mx list
    vector<string> mx_q;
    if(dns_query_t::mx_query(to_domain, mx_q, mx))
    {
        set_status_i(SMTP_FAILED, MX_S_TO_MX, 
                     "recipient address domain <%s> mx query failed",to_domain);
        logerror((MAILDROP, "smtp_delivery_t::send => mx_query failed."));
        return -1;
    }
    
    int mx_sock = connect_mx_i(mx_q);
    if(mx_sock < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_MX, 
                     " connect to mx domain <%s>  failed",to_domain);        
        logerror((MAILDROP, "smtp_delivery_t::send => connect_mx_i failed."));
        return -1;
    }
    string reason;
    string msg;
    //! read welcome response
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "recipient address domain <%s> mx query failed",to_domain);
        logerror((MAILDROP, "smtp_delivery_t::send => mx_query failed."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    // do HELO
    //msg = std::string("HELO ") + from_domain + "\r\n";
    msg = std::string("HELO ") + helo_host + "\r\n";
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing helo <%s> request",helo_host);
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_helo>."));
        return -1;
    }
    
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "timeout in reading <%s> welcome response",to_domain);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_helo>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    // make sure HELO returns what we expected
    int code = get_smtp_code_i(msg, reason);
    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_HELO, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_helo>."));
        return -1;
    }

    // do MAIL FROM
    msg = std::string("MAIL FROM: <") + from + ">\r\n";
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_mail_from>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "mail from <%s> response timeout",from);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_mail_from>."));
        return -1;        
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code >= 500) {
        set_status_i(SMTP_FAILED, MX_S_SMTP_MAIL, reason.c_str());  
        logerror((MAILDROP, "smtp_delivery_t::send blocked at <do_mail_from>."));
        return -1;
    }

    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_MAIL, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send temp failed at at <do_mail_from>."));
        return -1;
    }
    
    // do RCPT TO
    msg = std::string("RCPT TO: <") + to + ">\r\n";
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing rcpt to <%s> request",to);
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_rcpt_to>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "rcpt to <%s> response timeout",to);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_rcpt_to>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code >= 500) {
        set_status_i(SMTP_FAILED, MX_S_TO_USER, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send blocked at <do_rcpt_to>."));
        return -1;
    }
    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_RCPT, reason.c_str()); 
        logerror((MAILDROP, "smtp_delivery_t::send temp failed at <do_rcpt_to>."));
        return -1;
    }

    // do DATA
    msg = std::string("DATA\r\n");
    if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing data request");
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_data>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "data to <%s> response timeout",to);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_data>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code != 354) {
        set_status_i(SMTP_FAILED, MX_S_SMTP_DATA, reason.c_str());
       logerror((MAILDROP, "smtp_delivery_t::send <%s> blocked at <do_data>.",
                to));
        return -1;
    }

    // now send data over
    if(write_n(mx_sock, data.c_str(), data.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing data content request");
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_data_content>."));
        return -1;
    }
    
    // finish up
    msg = std::string("\r\n.\r\n");
     if(write_n(mx_sock, msg.c_str(), msg.size()) < 0)
    {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "failed in writing data end request");
        logerror((MAILDROP, "smtp_delivery_t::send => write failed at <do_data_dot>."));
        return -1;
    }
    if (read_socket_i(mx_sock, msg, timeout)) {
        set_status_i(SMTP_RETRY, MX_S_TO_FROZEN, 
                     "data end <%s> response timeout",to);
        logerror((MAILDROP, "smtp_delivery_t::send => read_socket timeout at <do_data>."));
        return -1;
    }
    logdebug((MAILDROP, "%s", msg.c_str()));
    code = get_smtp_code_i(msg, reason);
    if (code >= 500) {
        set_status_i(SMTP_FAILED, MX_S_SMTP_DOT, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send blocked at <do_data_dot>."));
        return -1;
    }
    if (code != 250) {
        set_status_i(SMTP_RETRY, MX_S_SMTP_DOT, reason.c_str());
        logerror((MAILDROP, "smtp_delivery_t::send temp failed at <do_data_dot>."));
        return -1;
    }
    logtrace((MAILDROP, "smtp_delivery_t::send end ok."));
    return 0;
}
int smtp_delivery_t::smtp_code()
{
    return m_smtp_code;
}
int smtp_delivery_t::status()
{
    logtrace((MAILDROP, "smtp_delivery_t::status"));
    return (m_mx_status + m_smtp_status);
}

const char* smtp_delivery_t::reason()
{
    logtrace((MAILDROP, "smtp_delivery_t::reason"));
    return m_reason.c_str();
}

